<?php

declare(strict_types=1);

namespace Core;

use PDO;

class App
{
    /** 默认命名空间、控制器和方法 */
    private const DEFAULT_BASE_NAMESPACE = 'App\\Controllers\\';
    private const DEFAULT_CONTROLLER     = 'Home';
    private const DEFAULT_METHOD         = 'index';

    /** 允许的控制器命名空间前缀 */
    private const ALLOWED_CONTROLLER_NAMESPACE = 'App\\Controllers\\';
    /** 定义入口文件名 */
    private const ENTRY_POINT_FILENAME = 'index.php';

    /** 单例实例 */
    private static ?self $instance = null;

    /** 应用配置 */
    private array $config;

    /** 路由配置 */
    private string $baseNamespace;
    private string $defaultClass;
    private string $defaultMethod;

    /** 数据库实例 */
    private PDO $db;

    /**
     * 私有构造函数 - 防止外部实例化
     *
     * @param array $config 配置数组
     */
    private function __construct(array $config = [])
    {
        // 合并配置，并直接赋值给提升的属性
        $this->config = array_merge([
            'base_namespace'     => self::DEFAULT_BASE_NAMESPACE,
            'default_controller' => self::DEFAULT_CONTROLLER,
            'default_method'     => self::DEFAULT_METHOD,
            'db_type'            => 'sqlite',
            'sqlite' => [
                'path' => __DIR__ . '/../data/db.sqlite',
            ],
            'mysql' => [
                'host'     => 'localhost',
                'dbname'   => 'demo',
                'username' => 'root',
                'password' => 'root',
                'port'     => 3306,
                'charset'  => 'utf8mb4',
            ],
            'view_path'          => __DIR__ . '/../app/views',
            'csrf_enabled'       => true,
            'xss_protection'     => true,
            'debug'              => false,
        ], $config);

        // 初始化提升的属性
        $this->baseNamespace = $this->config['base_namespace'];
        $this->defaultClass  = $this->config['default_controller'];
        $this->defaultMethod = $this->config['default_method'];

        // 启动会话 (如果需要 CSRF)
        if ($this->config['csrf_enabled']) {
            $this->startSession();
        }

        // 注册全局错误处理器
        $this->registerGlobalErrorHandler();

        // 初始化数据库
        $this->initDatabase();
    }

    /**
     * 获取单例实例
     * @param array $config 配置数组（仅首次调用有效）
     * @return self 单例实例
     */
    public static function getInstance(array $config = []): self
    {
        // 使用 null 合并赋值运算符简化单例逻辑
        return self::$instance ??= new self($config);
    }


    /**
     * 初始化数据库连接
     *
     * 根据 'db_type' 配置项选择 SQLite 或 MySQL 连接。
     * 默认使用 SQLite。
     *
     * @throws \RuntimeException 数据库连接失败时抛出
     */
    private function initDatabase(): void
    {
        $dbType = $this->config['db_type'] ?? 'sqlite'; // 默认为 sqlite

        $options = [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ];

        try {
            if ($dbType === 'sqlite') {
                $dbPath = $this->config['sqlite']['path'] ?? '';
                if (empty($dbPath)) {
                    throw new \RuntimeException("SQLite 数据库路径未配置或为空。");
                }
                $this->db = new PDO('sqlite:' . $dbPath, null, null, $options);
                // 可选：为 SQLite 设置外键约束（如果需要）
                // $this->db->exec("PRAGMA foreign_keys = ON;");

            } elseif ($dbType === 'mysql') {
                $mysqlConfig = $this->config['mysql'] ?? [];
                $host        = $mysqlConfig['host'] ?? '';
                $dbname      = $mysqlConfig['dbname'] ?? '';
                $username    = $mysqlConfig['username'] ?? '';
                $password    = $mysqlConfig['password'] ?? '';
                $port        = $mysqlConfig['port'] ?? 3306;
                $charset     = $mysqlConfig['charset'] ?? 'utf8mb4';

                if (empty($host) || empty($dbname) || empty($username)) {
                     throw new \RuntimeException("MySQL 数据库配置不完整 (缺少 host, dbname, 或 username)。");
                }

                // 构建 DSN (Data Source Name)
                $dsn = "mysql:host={$host};port={$port};dbname={$dbname};charset={$charset}";

                $this->db = new PDO($dsn, $username, $password, $options);
            } else {
                throw new \RuntimeException("不支持的数据库类型: {$dbType}");
            }
        } catch (\PDOException $e) {
            // 将 PDOException 转换为更通用的 RuntimeException 或自定义异常
            throw new \RuntimeException("数据库连接失败: " . $e->getMessage(), (int)$e->getCode(), $e);
        }
    }

    /**
     * 获取应用配置
     * @return array
     */
    public function getConfig(): array
    {
        return $this->config;
    }

    /**
     * 路由分发
     *
     * URL格式: /controller/method/param1/param2
     * 示例: /user/profile/123 -> UserController::profile(123)
     *
     * @return void
     */
    public function dispatch(): void
    {
        /**
         * 防止直接访问非入口文件
         * 
         * 通过检查当前执行的脚本是否是预定义的入口文件
         */
        $scriptFileName = $_SERVER['SCRIPT_FILENAME'] ?? '';
        $scriptName     = basename($scriptFileName);

        if ($scriptName !== self::ENTRY_POINT_FILENAME) {
            http_response_code(403);
            echo "403 Forbidden: Direct access to this resource is not allowed.";
            exit();
        }

        try {
            // 1. 获取并清理 URI
            $path = trim(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH), '/');
            if ($path === false) {
                $this->sendNotFound();
                return;
            }

            // 特殊处理根目录 "/"，使其变为空字符串
            $path = ($path === '/') ? '' : trim($path, '/');

            /**
             * 2. 解析 URI 片段
             * / -> []
             * /a -> ['a']
             * /a/b -> ['a', 'b']
             */
            $segments = ($path === '') ? [] : explode('/', $path);

            /**
             * 3. 确定控制器、方法和参数
             * array_shift() 会从数组中移除并返回第一个元素
             * 如果 $segments 为空, (null) ?: $this->defaultClass 会返回 'Home'
             * 如果 $segments 现在为空 (例如访问 /a), (null) ?: $this->defaultMethod 会返回 'index'
             * 剩下的片段都是参数
             */
            $classSegment  = array_shift($segments) ?? $this->defaultClass;
            $methodSegment = array_shift($segments) ?? $this->defaultMethod;
            $params        = $segments;

            // 4. 安全地格式化和验证类名与方法名
            $className  = $this->buildAndValidateClassName($classSegment);
            $methodName = $this->validateMethodName($methodSegment);

            // --- 安全性增强：检查命名空间 ---
            // if (strpos($className, self::ALLOWED_CONTROLLER_NAMESPACE) !== 0) {
            //     $this->sendError("Access Denied: Invalid controller namespace.");
            //     return; // 停止执行
            // }

            // 5. 验证并执行
            if (!class_exists($className)) {
                $this->sendNotFound("控制器不存在。{$className}");
                return;
            }

            $controller = new $className();

            if (!method_exists($controller, $methodName)) {
                $this->sendNotFound("方法不存在: {$methodName}");
                return;
            }
            // 6. 调用方法
            $controller->$methodName(...$params);
        } catch (\InvalidArgumentException $e) {
            // 捕获来自验证方法的特定异常
            $this->sendNotFound($e->getMessage());
        }
    }

    /**
     * 构建并验证控制器类名。
     *
     * @param string $classSegment URI 中的类名片段
     * @return string 完整且经过验证的类名
     * @throws \InvalidArgumentException 如果类名无效
     */
    private function buildAndValidateClassName(string $classSegment): string
    {
        // 移除非法字符 (只允许 字母, 数字, 下划线, 反斜杠)
        $cleanedSegment = preg_replace('/[^a-zA-Z0-9_\\\\]/', '', $classSegment);
        if ($cleanedSegment !== $classSegment) {
            throw new \InvalidArgumentException("Invalid characters in controller name segment.");
        }

        // 简单的防止目录遍历
        if (str_contains($classSegment, '..') || str_contains($classSegment, './')) {
            throw new \InvalidArgumentException("检测到潜在的目录遍历尝试。");
        }

        // 格式化类名 (首字母大写)
        return $this->config['base_namespace'] . ucfirst($cleanedSegment);
    }

    /**
     * 验证方法名
     *
     * @param string $segment URI 中的方法名称
     * @return string 验证后的方法名
     * @throws InvalidArgumentException 方法名无效时抛出
     */
    private function validateMethodName(string $segment): string
    {
        // 移除非法字符 (只允许 字母, 数字, 下划线)
        $cleanedMethod = preg_replace('/[^a-zA-Z0-9_]/', '', $segment);

        if ($cleanedMethod !== $segment) {
            throw new \InvalidArgumentException("方法名称段中的字符无效。");
        }

        // 如果清理后为空 (例如, 只有非法字符), 使用默认方法
        return $cleanedMethod ?: $this->config['default_method'];
    }

    // ==================== 数据库操作方法 ====================

    /**
     * 执行参数化查询（防SQL注入）
     *
     * @param string $sql SQL 语句
     * @param array $params 参数数组
     * @return PDOStatement
     * @throws \PDOException SQL 执行失败时抛出
     */
    public function query(string $sql, array $params = []): \PDOStatement
    {
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        return $stmt;
    }

    /**
     * 插入数据
     *
     * @param string $table 表名（需预先验证）
     * @param array $data 键值对数组
     * @return int 插入的记录 ID
     */
    public function insert(string $table, array $data): int
    {
        $this->validateTableName($table);

        $columns      = implode(', ', array_keys($data));
        $placeholders = ':' . implode(', :', array_keys($data));
        $sql          = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";

        $this->query($sql, $data);
        return (int) $this->db->lastInsertId();
    }

    /**
     * 更新数据
     *
     * @param string $table 表名
     * @param array $data 要更新的数据
     * @param array $where 条件数组
     * @return int 影响的行数
     */
    public function update(string $table, array $data, array $where): int
    {
        $this->validateTableName($table);

        $set         = implode(', ', array_map(fn ($k) => "{$k} = :{$k}", array_keys($data)));
        $whereClause = implode(' AND ', array_map(fn ($k) => "{$k} = :w_{$k}", array_keys($where)));
        $sql         = "UPDATE {$table} SET {$set} WHERE {$whereClause}";

        // 合并参数，为 where 条件添加前缀避免冲突
        $params = $data;
        foreach ($where as $key => $value) {
            $params["w_{$key}"] = $value;
        }

        return $this->query($sql, $params)->rowCount();
    }

    /**
     * 删除数据
     *
     * @param string $table 表名
     * @param array $where 条件数组
     * @return int 影响的行数
     */
    public function delete(string $table, array $where): int
    {
        $this->validateTableName($table);

        $whereClause = implode(' AND ', array_map(fn ($k) => "{$k} = :{$k}", array_keys($where)));
        $sql         = "DELETE FROM {$table} WHERE {$whereClause}";

        return $this->query($sql, $where)->rowCount();
    }

    /**
     * 验证表名安全性（防止 SQL 注入）
     *
     * @param string $table 表名
     * @throws InvalidArgumentException 表名无效时抛出
     */
    private function validateTableName(string $table): void
    {
        if (!preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
            throw new \InvalidArgumentException('表名包含非法字符');
        }
    }

    // ==================== 响应和输入处理 ====================

    /**
     * 输出 JSON 响应
     *
     * @param mixed $data 要输出的数据
     * @param int $code HTTP 状态码
     */
    public function json(mixed $data, int $code = 200): void
    {
        http_response_code($code);
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    /**
     * 获取用户输入（支持 XSS 过滤）
     *
     * @param string $key 键名
     * @param mixed $default 默认值
     * @param bool $xssFilter 是否过滤 XSS
     * @return mixed
     */
    public function input(string $key, mixed $default = null, bool $xssFilter = true): mixed
    {
        $value = $_POST[$key] ?? $_GET[$key] ?? $default;

        if ($xssFilter && is_string($value)) {
            $value = htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
        }

        return $value;
    }

    /**
     * 验证输入数据
     *
     * @param array $rules 验证规则 ['field' => 'required|email|min:5']
     * @return array 返回 ['valid' => bool, 'data' => array, 'errors' => array]
     */
    public function validate(array $rules): array
    {
        $errors = [];
        $data   = [];

        foreach ($rules as $field => $rule) {
            $value    = $this->input($field, '', false); // 获取原始值
            $ruleList = is_array($rule) ? $rule : explode('|', $rule);

            foreach ($ruleList as $r) {
                if ($r === 'required' && empty($value)) {
                    $errors[$field][] = "{$field} 是必填项";
                }

                if (str_starts_with($r, 'min:')) {
                    $min = (int) substr($r, 4);
                    if (strlen((string) $value) < $min) {
                        $errors[$field][] = "{$field} 最少 {$min} 个字符";
                    }
                }

                if (str_starts_with($r, 'max:')) {
                    $max = (int) substr($r, 4);
                    if (strlen((string) $value) > $max) {
                        $errors[$field][] = "{$field} 最多 {$max} 个字符";
                    }
                }

                if ($r === 'email' && !empty($value) && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
                    $errors[$field][] = "{$field} 必须是有效的邮箱地址";
                }

                if ($r === 'numeric' && !empty($value) && !is_numeric($value)) {
                    $errors[$field][] = "{$field} 必须是数字";
                }
            }

            $data[$field] = $value;
        }

        return [
            'valid'  => empty($errors),
            'data'   => $data,
            'errors' => $errors,
        ];
    }

    // ==================== 安全功能 ====================

    /**
     * 启动会话并生成 CSRF Token
     */
    private function startSession(): void
    {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }

        if (!isset($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
    }

    /**
     * 获取 CSRF Token
     *
     * @return string
     */
    public function generateCsrfToken(): string
    {
        $this->startSession();
        return $_SESSION['csrf_token'] ?? '';
    }

    /**
     * 验证 CSRF Token（防止跨站请求伪造）
     *
     * @param string|null $token 待验证的 token（为空则自动从请求中获取）
     * @return bool
     */
    public function validateCsrfToken(?string $token = null): bool
    {
        if (!$this->config['csrf_enabled']) {
            return true;
        }

        $token ??= $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';

        if (empty($token) || empty($_SESSION['csrf_token'])) {
            return false;
        }

        // 使用时间安全比较防止时序攻击
        return hash_equals($_SESSION['csrf_token'], $token);
    }

    // ==================== 错误处理 ====================

    /**
     * 发送 404 响应
     *
     * @param string|null $message 错误消息（调试模式下显示）
     */
    private function sendNotFound(?string $message = null): void
    {
        http_response_code(404);
        echo $message ?? '404 页面未找到';
        exit;
    }

    /**
     * [全局异常处理器] 发送 500 响应
     * @param \Throwable $e 异常对象
     * @return void
     */
    public function handleException(\Throwable $e): void
    {
        http_response_code(500);

        $message = $this->config['debug'] ? '500 Internal Server Error' : '服务器内部错误';

        if ($this->config['debug']) {
            echo "<h1>{$message}</h1>";
            echo "<p><b>Exception:</b> " . get_class($e) . "</p>";
            echo "<p><b>Message:</b> {$e->getMessage()}</p>";
            echo "<p><b>File:</b> {$e->getFile()} : {$e->getLine()}</p>";
            echo "<pre>{$e->getTraceAsString()}</pre>";
        } else {
            // 在生产环境中，你可能想将 $e->getMessage() 记录到日志文件
            echo $message;
        }
        exit;
    }

    /**
     * [全局错误处理器] 将 PHP 错误转换为异常
     * @throws \ErrorException
     */
    public function handleError(int $errno, string $errstr, string $errfile, int $errline): bool
    {
        if (!(error_reporting() & $errno)) {
            // 此错误级别未被 error_reporting 包含
            return false;
        }
        throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
    }

    /**
     * 注册全局错误和异常处理器
     * @return void
     */
    private function registerGlobalErrorHandler(): void
    {
        set_exception_handler([$this, 'handleException']);
        set_error_handler([$this, 'handleError']);
    }
}
